package com.ihr360.job.core.step;

import com.ihr360.job.core.entity.StepContribution;
import com.ihr360.job.core.item.Chunk;
import com.ihr360.job.core.item.ItemReader;
import com.ihr360.job.core.item.SimpleChunkProvider;
import com.ihr360.job.core.item.SkipOverflowException;
import com.ihr360.job.core.repeat.RepeatOperations;
import com.ihr360.job.core.scope.context.SimpleStepContext;
import com.ihr360.job.core.step.skip.LimitCheckingItemSkipPolicy;
import com.ihr360.job.core.step.skip.NonSkippableReadException;
import com.ihr360.job.core.step.skip.SkipException;
import com.ihr360.job.core.step.skip.SkipListenerFailedException;
import com.ihr360.job.core.step.skip.SkipPolicy;
import com.ihr360.job.core.step.skip.SkipPolicyFailedException;
import org.springframework.classify.BinaryExceptionClassifier;
import org.springframework.classify.Classifier;

/**
 * FaultTolerant implementation of the {@link ChunkProcessor} interface, that
 * allows for skipping or retry of items that cause exceptions during reading or
 * processing.
 *
 */
public class FaultTolerantChunkProvider<I> extends SimpleChunkProvider<I> {

    /**
     * Hard limit for number of read skips in the same chunk. Should be
     * sufficiently high that it is only encountered in a runaway step where all
     * items are skipped before the chunk can complete (leading to a potential
     * heap memory problem).
     */
    public static final int DEFAULT_MAX_SKIPS_ON_READ = 100;

    private SkipPolicy skipPolicy = new LimitCheckingItemSkipPolicy();

    private Classifier<Throwable, Boolean> rollbackClassifier = new BinaryExceptionClassifier(true);

    private int maxSkipsOnRead = DEFAULT_MAX_SKIPS_ON_READ;

    public FaultTolerantChunkProvider(ItemReader<? extends I> itemReader, RepeatOperations repeatOperations) {
        super(itemReader, repeatOperations);
    }

    /**
     * @param maxSkipsOnRead the maximum number of skips on read
     */
    public void setMaxSkipsOnRead(int maxSkipsOnRead) {
        this.maxSkipsOnRead = maxSkipsOnRead;
    }

    /**
     * The policy that determines whether exceptions can be skipped on read.
     * @param SkipPolicy
     */
    public void setSkipPolicy(SkipPolicy SkipPolicy) {
        this.skipPolicy = SkipPolicy;
    }

    /**
     * Classifier to determine whether exceptions have been marked as
     * no-rollback (as opposed to skippable). If encountered they are simply
     * ignored, unless also skippable.
     *
     * @param rollbackClassifier the rollback classifier to set
     */
    public void setRollbackClassifier(Classifier<Throwable, Boolean> rollbackClassifier) {
        this.rollbackClassifier = rollbackClassifier;
    }

    @Override
    protected I read(StepContribution contribution, Chunk<I> chunk,SimpleStepContext simpleStepContext) throws Exception {
        while (true) {
            try {
                return doRead(simpleStepContext);
            }
            catch (Exception e) {

                if (shouldSkip(skipPolicy, e, contribution.getStepSkipCount())) {

                    // increment skip count and try again
                    contribution.incrementReadSkipCount();
                    chunk.skip(e);

                    if (chunk.getErrors().size() >= maxSkipsOnRead) {
                        throw new SkipOverflowException("Too many skips on read");
                    }

                    logger.debug("Skipping failed input", e);
                }
                else {
                    if (rollbackClassifier.classify(e)) {
                        throw new NonSkippableReadException("Non-skippable exception during read", e);
                    }
                    logger.debug("No-rollback for non-skippable exception (ignored)", e);
                }

            }
        }
    }

    @Override
    public void postProcess(StepContribution contribution, Chunk<I> chunk) {
        for (Exception e : chunk.getErrors()) {
            try {
                getListener().onSkipInRead(e);
            }
            catch (RuntimeException ex) {
                throw new SkipListenerFailedException("Fatal exception in SkipListener.", ex, e);
            }
        }
    }

    /**
     * Convenience method for calling process skip policy.
     *
     * @param policy the skip policy
     * @param e the cause of the skip
     * @param skipCount the current skip count
     */
    private boolean shouldSkip(SkipPolicy policy, Throwable e, int skipCount) {
        try {
            return policy.shouldSkip(e, skipCount);
        }
        catch (SkipException ex) {
            throw ex;
        }
        catch (RuntimeException ex) {
            throw new SkipPolicyFailedException("Fatal exception in SkipPolicy.", ex, e);
        }
    }

}
